#region License

//Copyright © 2014 All Right Reserved, Derek Goslin
// 
// Microsoft Public License (Ms-PL)

// This license governs use of the accompanying software. If you use the software, you accept this license. If you
// do not accept the license, do not use the software.

// 1. Definitions

// The terms "reproduce," "reproduction," "derivative works," and "distribution" have the same meaning here as
// under U.S. copyright law.

// A "contribution" is the original software, or any additions or changes to the software.

// A "contributor" is any person that distributes its contribution under this license.

// "Licensed patents" are a contributor's patent claims that read directly on its contribution.

// 2. Grant of Rights

// (A) Copyright Grant- Subject to the terms of this license, including the license conditions and limitations in
// section 3, each contributor grants you a non-exclusive, worldwide, royalty-free copyright license to reproduce
// its contribution, prepare derivative works of its contribution, and distribute its contribution or any derivative
// works that you create.

// (B) Patent Grant- Subject to the terms of this license, including the license conditions and limitations in section 3,
// each contributor grants you a non-exclusive, worldwide, royalty-free license under its licensed patents to make, have made,
// use, sell, offer for sale, import, and/or otherwise dispose of its contribution in the software or 
// derivative works of the contribution in the software.

// 3. Conditions and Limitations

// (A) No Trademark License- This license does not grant you rights to use any contributors' name, logo, or trademarks.

// (B) If you bring a patent claim against any contributor over patents that you claim are infringed by the software,
// your patent license from such contributor to the software ends automatically.

// (C) If you distribute any portion of the software, you must retain all copyright, patent, trademark, and attribution
// notices that are present in the software.

// (D) If you distribute any portion of the software in source code form, you may do so only under this license by
// including a complete copy of this license with your distribution. If you distribute any portion of the software
// in compiled or object code form, you may only do so under a license that complies with this license.

// (E) The software is licensed "as-is." You bear the risk of using it. The contributors give no express warranties,
// guarantees or conditions. You may have additional consumer rights under your local laws which this license cannot
// change. To the extent permitted under your local laws, the contributors exclude the implied warranties of
// merchantability, fitness for a particular purpose and non-infringement.

#endregion

using System;
using Microsoft.SPOT;
using Microsoft.SPOT.Hardware;
using mfNordic.Radios.Internals;

namespace mfNordic.Radios
{
    public class Nrf24L01P
    {
        #region Delegates

        public delegate void OnDataReceiveFailureEventHandler(object sender, DataReceiveFailureEventArgs e);

        public delegate void OnDataReceivedEventHandler(object sender, DataReceivedEventArgs e);

        public delegate void OnTransmitFailedEventHandler(object sender, TransmitFailedEventArgs e);

        public delegate void OnTransmitSucesssEventHandler(object sender, EventArgs e);

        #endregion

        private const Int32 MaxPayload = 32;

        public const Int32 TxFifoSize = 3;

        private readonly OutputPort _chipEnablePort;
        private readonly DataPipe[] _dataPipes;
        private readonly InterruptPort _interruptPin;
        private readonly SPI _spi;

        /// <summary>
        /// Initializes a new instance of the <see cref="Nrf24L01P"/> class.
        /// </summary>
        /// <param name="spi">The <see cref="SPI.SPI_module"/> to use to communicate to the radio device</param>
        /// <param name="chipSelectPin">The <see cref="Cpu.Pin"/> that the radio devices chip select pin is connected</param>
        /// <param name="chipEnablePin">The <see cref="Cpu.Pin"/> that the radio devices chip enable pin is connected</param>
        /// <param name="interruptPin">The <see cref="Cpu.Pin"/> that the radio devices interrupt pin is connected</param>
        public Nrf24L01P(SPI.SPI_module spi, Cpu.Pin chipSelectPin, Cpu.Pin chipEnablePin, Cpu.Pin interruptPin)
        {
            _spi = new SPI(new SPI.Configuration(chipSelectPin, false, 0, 0, false, true, 2000, spi));

            _interruptPin = new InterruptPort(interruptPin, false, Port.ResistorMode.PullUp,
                                              Port.InterruptMode.InterruptEdgeLow);

            _interruptPin.OnInterrupt += HandleInterrupt;

            _chipEnablePort = new OutputPort(chipEnablePin, false);

            _dataPipes = new DataPipe[(Int32) Pipe.Pipe5 + 1];

            for (Int32 i = 0; i <= (Int32) Pipe.Pipe5; i++)
            {
                _dataPipes[i] = new DataPipe(this, (Pipe) i);
            }
        }

        /// <summary>
        /// Power up the device
        /// </summary>
        public Boolean PowerUp
        {
            get { return GetRegisterBooleanValue(Registers.CONFIG, BitFlags.PWR_UP); }
            set { ApplyRegistryBitFlag(Registers.CONFIG, BitFlags.PWR_UP, value); }
        }

        /// <summary>
        /// Chip Enable Activates RX or TX mode
        /// </summary>
        /// <remarks>
        /// For a module operating in transmit mode this should not be set to true. From the NRF24L01P documentation ->
        /// The nRF24L01+ transmitter PLL operates in open loop when in TX mode. 
        /// It is important never to keep the nRF24L01+ in TX mode for more than 4ms at a time. 
        /// If the Enhanced ShockBurst™ features are enabled, nRF24L01+ is never in TX mode longer than 4ms.
        /// </remarks>
        public Boolean Enable
        {
            get { return _chipEnablePort.Read(); }
            set { _chipEnablePort.Write(value); }
        }

        /// <summary>
        /// Get or sets the frequency channel nRF24L01+ operates on
        /// </summary>
        public Byte Channel
        {
            get { return (Byte) (ExecuteCommand(Commands.R_REGISTER, Registers.RF_CH, new Byte[1])[1] & 0x7F); }
            set { ExecuteCommand(Commands.W_REGISTER, Registers.RF_CH, new[] {(Byte) (value & 0x7F)}); }
        }

        /// <summary>
        /// Gets or sets the radio <see cref="Mode"/>
        /// </summary>
        public Mode Mode
        {
            get
            {
                return
                    (Mode)
                    (ExecuteCommand(Commands.R_REGISTER, Registers.CONFIG, new Byte[1])[1] & (1 << BitFlags.PRIM_RX));
            }
            set { ApplyRegistryBitFlag(Registers.CONFIG, BitFlags.PRIM_RX, value == Mode.Receiver); }
        }

        /// <summary>
        /// Gets or sets the radio packet <see cref="AddressWidth"/>>
        /// </summary>
        public AddressWidth AddressWidth
        {
            get { return (AddressWidth) (ExecuteCommand(Commands.R_REGISTER, Registers.SETUP_AW, new Byte[1])[1]); }
            set { ExecuteCommand(Commands.W_REGISTER, Registers.SETUP_AW, new[] {(Byte) value}); }
        }

        /// <summary>
        /// Gets or sets the <see cref="ReTransmitDelay"/>
        /// </summary>
        public ReTransmitDelay AutoRetransDelay
        {
            get
            {
                return
                    (ReTransmitDelay)
                    (ExecuteCommand(Commands.R_REGISTER, Registers.SETUP_RETR, new Byte[1])[1] >> BitFlags.ARD);
            }
            set
            {
                Byte current = ExecuteCommand(Commands.R_REGISTER, Registers.SETUP_RETR, new Byte[1])[1];

                current &= 0x0F;

                current |= (Byte) ((Byte) value << BitFlags.ARD);

                ExecuteCommand(Commands.W_REGISTER, Registers.SETUP_RETR, new[] {current});
            }
        }

        /// <summary>
        /// Gets or sets the auto retransmit count
        /// </summary>
        public Byte AutoRetransmitCount
        {
            get { return (Byte) (ExecuteCommand(Commands.R_REGISTER, Registers.SETUP_RETR, new Byte[1])[1] & 0x0F); }
            set
            {
                Byte current = ExecuteCommand(Commands.R_REGISTER, Registers.SETUP_RETR, new Byte[1])[1];

                current &= 0xF0;
                current |= (Byte) (value & 0x0F);

                ExecuteCommand(Commands.W_REGISTER, Registers.SETUP_RETR, new[] {current});
            }
        }

        /// <summary>
        /// Gets or sets the radio <see cref="DataRate"/>
        /// </summary>
        public DataRate DataRate
        {
            get
            {
                var current = (Byte) (ExecuteCommand(Commands.R_REGISTER, Registers.RF_SETUP, new Byte[1])[1] & 0x28);

                Byte rate = 0;
                rate |= (Byte) (current >> BitFlags.RF_DR_LOW);
                current &= 0x08;
                rate |= (Byte) (current >> BitFlags.RF_DR_HIGH - 1);
                return (DataRate) rate;
            }
            set
            {
                Byte current = ExecuteCommand(Commands.R_REGISTER, Registers.RF_SETUP, new Byte[1])[1];

                switch (value)
                {
                    case DataRate.OneMbps:
                        current &= 0xD7;
                        break;
                    case DataRate.TwoMbps:
                        current &= 0xDF;
                        current |= 0x08;
                        break;
                    case DataRate.TwoHundredFiftyKbps:
                        current &= 0xF7;
                        current |= 0x20;
                        break;
                }

                ExecuteCommand(Commands.W_REGISTER, Registers.RF_SETUP, new[] {current});
            }
        }

        /// <summary>
        /// Gets or sets the radio <see cref="TransmitPower"/>
        /// </summary>
        public TransmitPower TransmitPower
        {
            get
            {
                return
                    (TransmitPower)
                    ((ExecuteCommand(Commands.R_REGISTER, Registers.RF_SETUP, new Byte[1])[1] >> 1) & 0x03);
            }
            set
            {
                Byte current = ExecuteCommand(Commands.R_REGISTER, Registers.RF_SETUP, new Byte[1])[1];

                current &= 0xF8;
                current |= (Byte) ((Byte) value << 1);

                ExecuteCommand(Commands.W_REGISTER, Registers.RF_SETUP, new[] {current});
            }
        }

        /// <summary>
        /// Gets the <see cref="Status"/>
        /// </summary>
        public Status Status
        {
            get
            {
                var readBuffer = new Byte[1];
                _spi.WriteRead(new[] {(Byte) Commands.NOP}, readBuffer);

                return new Status(readBuffer[0]);
            }
        }

        /// <summary>
        /// Get the <see cref="TransmitObservation"/>
        /// </summary>
        public TransmitObservation TransmitObservation
        {
            get
            {
                Byte current = ExecuteCommand(Commands.R_REGISTER, Registers.OBSERVE_TX, new Byte[1])[1];

                return new TransmitObservation(((current & 0xF0) >> BitFlags.PLOS_CNT), (current & 0x0F));
            }
        }

        /// <summary>
        /// Gets the Received Power Detector status
        /// </summary>
        public Boolean ReceivePowerDetect
        {
            get { return GetRegisterBooleanValue(Registers.RPD, BitFlags.RPD); }
        }

        /// <summary>
        /// Gets or sets the radios transmit address
        /// </summary>
        /// <exception cref="InvalidAddressSizeException">Thrown if the address size does not match the device configuration</exception>
        public Byte[] TransmitAddress
        {
            get
            {
                AddressWidth addressWidth = AddressWidth;

                return GetAddressRegister(Registers.TX_ADDR, (Byte) (addressWidth + 2));
            }
            set
            {
                var addressSize =
                    (Byte) (ExecuteCommand(Commands.R_REGISTER, Registers.SETUP_AW, new Byte[1])[1] & 0x03);

                if (value.Length != addressSize + 2)
                {
                    throw new InvalidAddressSizeException("Invalid address size", addressSize);
                }

                ExecuteCommand(Commands.W_REGISTER, Registers.TX_ADDR, addressSize > 1 ? ReverseArray(value) : value);
            }
        }

        /// <summary>
        /// Gets or sets the CRC scheme used by the radio
        /// </summary>
        public CrcScheme CrcScheme
        {
            get
            {
                return
                    (CrcScheme)
                    ((ExecuteCommand(Commands.R_REGISTER, Registers.CONFIG, new Byte[1])[1] & BitFlags.CRCO) >>
                     BitFlags.CRCO);
            }
            set { ApplyRegistryBitFlag(Registers.CONFIG, BitFlags.CRCO, value == CrcScheme.TwoByte); }
        }

        /// <summary>
        /// The set of <see cref="DataPipe"/> instances that this radio controls
        /// </summary>
        /// <param name="id">The identifier for an instance of <see cref="DataPipe"/></param>
        /// <returns>A <see cref="DataPipe"/> instance</returns>
        public DataPipe this[Pipe id]
        {
            get { return _dataPipes[(Byte) id]; }
        }

        /// <summary>
        /// Masks the interrupt caused by RX_DR
        /// </summary>
        public Boolean MaskRxDr
        {
            get { return GetRegisterBooleanValue(Registers.CONFIG, BitFlags.MASK_RX_DR); }
            set { ApplyRegistryBitFlag(Registers.CONFIG, BitFlags.MASK_RX_DR, value); }
        }

        /// <summary>
        /// Masks the interrupt caused by TX_DS
        /// </summary>
        public Boolean MaskTxDx
        {
            get { return GetRegisterBooleanValue(Registers.CONFIG, BitFlags.MASK_TX_DS); }
            set { ApplyRegistryBitFlag(Registers.CONFIG, BitFlags.MASK_TX_DS, value); }
        }

        /// <summary>
        /// Masks the interrupt caused by TX_DS
        /// </summary>
        public Boolean MaskMaxRt
        {
            get { return GetRegisterBooleanValue(Registers.CONFIG, BitFlags.MASK_MAX_RT); }
            set { ApplyRegistryBitFlag(Registers.CONFIG, BitFlags.MASK_MAX_RT, value); }
        }

        /// <summary>
        /// Enable dynamic acknowledge on transmit packets
        /// </summary>
        public Boolean DynamicAckEnabled
        {
            get { return GetRegisterBooleanValue(Registers.FEATURE, BitFlags.EN_DYN_ACK); }
            set { ApplyRegistryBitFlag(Registers.FEATURE, BitFlags.EN_DYN_ACK, value); }
        }

        /// <summary>
        /// Enable acknowledge payloads
        /// </summary>
        public Boolean AckPayloadEnabled
        {
            get { return GetRegisterBooleanValue(Registers.FEATURE, BitFlags.EN_ACK_PAY); }
            set { ApplyRegistryBitFlag(Registers.FEATURE, BitFlags.EN_ACK_PAY, value); }
        }

        /// <summary>
        /// Enable dynamic payload data length
        /// </summary>
        public Boolean DynamicPayloadEnabled
        {
            get { return GetRegisterBooleanValue(Registers.FEATURE, BitFlags.EN_DPL); }
            set { ApplyRegistryBitFlag(Registers.FEATURE, BitFlags.EN_DPL, value); }
        }

        /// <summary>
        /// Write a packet to the transmit FIFO
        /// </summary>
        /// <param name="data">The data to write to the transmit FIFO</param>
        /// <param name="acknowledge">Indicates if the packet should be acknowledged with an ACK packet from the receiver</param>
        /// <remarks>The radio must be enabled and configured as transmitter, the <see cref="DynamicAckEnabled"/> must be set to true</remarks>
        public void WriteTransmitPayload(Byte[] data, Boolean acknowledge)
        {
            if (data.Length > MaxPayload)
            {
                throw new ArgumentOutOfRangeException("data", "data must be less than 32 bytes in length");
            }

            ExecuteCommand(acknowledge ? Commands.W_TX_PAYLOAD : Commands.W_TX_PAYLOAD_NO_ACK, 0x00, data);
        }
        
        /// <summary>
        /// Reverse the order of an input byte array
        /// </summary>
        /// <param name="byteArray">The byte array to reverse</param>
        /// <returns>The <paramref name="byteArray"/> reversed</returns>
        /// <remarks>This implementation reverses the byte array in situ</remarks>
        public static Byte[] ReverseArray(Byte[] byteArray)
        {
            Int32 low = 0;
            Int32 high = byteArray.Length - 1;

            while (low < high)
            {
                Byte swap = byteArray[low];
                byteArray[low++] = byteArray[high];
                byteArray[high--] = swap;
            }

            return byteArray;
        }

        private void HandleInterrupt(uint data1, uint data2, DateTime time)
        {
            Status statusSnapshot = Status;

            if (Status.TxFull)
            {
                ExecuteCommand(Commands.FLUSH_TX, 0x00, new Byte[0]);

                RaiseEvent(OnTransmitFailed, new TransmitFailedEventArgs(statusSnapshot));
            }

            if (Status.ResendLimitReached)
            {
                ExecuteCommand(Commands.FLUSH_TX, 0x00, new Byte[0]);
                ExecuteCommand(Commands.W_REGISTER, Registers.STATUS, new[] {(Byte) (1 << BitFlags.MAX_RT)});

                RaiseEvent(OnTransmitFailed, new TransmitFailedEventArgs(statusSnapshot));
            }

            if (Status.DataSent)
            {
                ExecuteCommand(Commands.W_REGISTER, Registers.STATUS, new[] {(Byte) (1 << BitFlags.TX_DS)});
                RaiseEvent(OnTransmitSuccess, EventArgs.Empty);
            }

            if (Status.DataReady)
            {
                var pipeId = (Pipe) Status.DataPipe;

                while (!Status.RxEmpty)
                {
                    Byte payloadLen = ExecuteCommand(Commands.R_RX_PL_WID, 0x00, new Byte[1])[1];

                    if (payloadLen > 32)
                    {
                        ExecuteCommand(Commands.FLUSH_RX, 0x00, new Byte[0]);

                        RaiseEvent(OnDataReceiveFailure, new DataReceiveFailureEventArgs(statusSnapshot, pipeId));
                    }
                    else
                    {
                        ExecuteCommand(Commands.W_REGISTER, Registers.STATUS, new[] {(Byte) (1 << BitFlags.RX_DR)});

                        var payload = new Byte[payloadLen];

                        Array.Copy(ExecuteCommand(Commands.R_RX_PAYLOAD, 0x00, new Byte[payloadLen]), 1, payload, 0,
                                   payloadLen);

                        RaiseEvent(OnDataReceived, new DataReceivedEventArgs(pipeId, payload));
                    }
                }
            }
        }

        private void RaiseEvent(OnDataReceiveFailureEventHandler eventHandler, DataReceiveFailureEventArgs eventArgs)
        {
            if (eventHandler != null)
            {
                eventHandler(this, eventArgs);
            }
        }

        private void RaiseEvent(OnTransmitFailedEventHandler eventHandler, TransmitFailedEventArgs eventArgs)
        {
            if (eventHandler != null)
            {
                eventHandler(this, eventArgs);
            }
        }

        private void RaiseEvent(OnDataReceivedEventHandler eventHandler, DataReceivedEventArgs eventArgs)
        {
            if (eventHandler != null)
            {
                eventHandler(this, eventArgs);
            }
        }

        private void RaiseEvent(OnTransmitSucesssEventHandler eventHandler, EventArgs eventArgs)
        {
            if (eventHandler != null)
            {
                eventHandler(this, eventArgs);
            }
        }

        private Byte[] ExecuteCommand(Commands command, Registers register, Byte[] data)
        {
            var writeBuffer = new Byte[data.Length + 1];
            var readBuffer = new Byte[data.Length + 1];

            writeBuffer[0] = (Byte) ((Byte) command | (Byte) register);

            Array.Copy(data, 0, writeBuffer, 1, data.Length);

            _spi.WriteRead(writeBuffer, readBuffer);

            return readBuffer;
        }

        private Byte[] GetAddressRegister(Registers register, Byte addressSize)
        {
            var buffer = new Byte[addressSize];

            Array.Copy(ExecuteCommand(Commands.R_REGISTER, register, buffer), 1, buffer, 0, buffer.Length);

            return addressSize > 1 ? ReverseArray(buffer) : buffer;
        }
        
        internal void ApplyRegistryBitFlag(Registers register, Int32 flag, Boolean flagValue)
        {
            Byte current = ExecuteCommand(Commands.R_REGISTER, register, new Byte[1])[1];

            if (flagValue)
            {
                current |= (Byte) (1 << flag);
            }
            else
            {
                current &= (Byte) (~(1 << flag));
            }

            ExecuteCommand(Commands.W_REGISTER, register, new[] {current});
        }

        /// <summary>
        /// Raised when a receive error occurs.
        /// <remarks>
        /// Raised when a packet of lenght > 32 is received.
        /// The RX fifo is flushed in this case, as per the guidance in NRF24L01P specification
        /// </remarks>
        /// </summary>
        public event OnDataReceiveFailureEventHandler OnDataReceiveFailure;

        /// <summary>
        /// Raised when transmission of a packet succeeds
        /// </summary>
        public event OnTransmitSucesssEventHandler OnTransmitSuccess;
        
        /// <summary>
        /// Raised when transmission of a packet fails.
        /// <remarks>Transmission of a packet can fail due to:
        /// Exceeding the number of retries with out an ACK
        /// The TX fifo is full.
        /// In the case the TX fifo is full it will be flushed
        /// In the case of resend limit exceeded the TX fifo will be flused
        /// The <see cref="Status"/> property of the <see cref="TransmitFailedEventArgs"/> will indicate the reason for the event
        /// </remarks>
        /// </summary>
        public event OnTransmitFailedEventHandler OnTransmitFailed;

        /// <summary>
        /// Raised when data is received on a <see cref="DataPipe"/>
        /// </summary>
        public event OnDataReceivedEventHandler OnDataReceived;

        private Boolean GetRegisterBooleanValue(Registers register, Int32 bitPosition)
        {
            var mask = (Byte) (1 << bitPosition);

            Int32 result = ExecuteCommand(Commands.R_REGISTER, register, new Byte[1])[1] & mask;

            return (result >> bitPosition) == 1;
        }

        #region Nested type: DataPipe

        public class DataPipe
        {
            private readonly Byte _pipeId;
            private readonly Nrf24L01P _radio;

            internal DataPipe(Nrf24L01P radio, Pipe pipeNumber)
            {
                _radio = radio;
                PipeNumber = pipeNumber;
                _pipeId = (Byte) pipeNumber;
            }

            public Pipe PipeNumber { get; set; }

            /// <summary>
            /// Enable Auto Acknowledgement on this <see cref="DataPipe"/>
            /// </summary>
            public Boolean AutoAcknowledgment
            {
                get { return _radio.GetRegisterBooleanValue(Registers.EN_AA, _pipeId); }
                set { _radio.ApplyRegistryBitFlag(Registers.EN_AA, _pipeId, value); }
            }

            /// <summary>
            /// Enable this <see cref="DataPipe"/>
            /// </summary>
            public Boolean Enabled
            {
                get { return _radio.GetRegisterBooleanValue(Registers.EN_RXADDR, _pipeId); }
                set { _radio.ApplyRegistryBitFlag(Registers.EN_RXADDR, _pipeId, value); }
            }

            /// <summary>
            /// Enables dynamic payload
            /// </summary>
            public Boolean DynamicPayload
            {
                get { return _radio.GetRegisterBooleanValue(Registers.DYNPD, _pipeId); }
                set { _radio.ApplyRegistryBitFlag(Registers.DYNPD, _pipeId, value); }
            }

            /// <summary>
            /// Get or set the receiving address for this <see cref="DataPipe"/> instance
            /// </summary>
            /// <exception cref="InvalidAddressSizeException">If the address value set does not match the <see cref="AddressWidth"/> value</exception>
            public Byte[] ReceiveAddress
            {
                get
                {
                    Byte addressSize = 1;

                    if (_pipeId <= (Byte) Pipe.Pipe1)
                    {
                        addressSize = (Byte) (_radio.AddressWidth + 2);
                    }

                    return _radio.GetAddressRegister((Registers) (_pipeId + (Byte) Registers.RX_ADDR_P0), addressSize);
                }
                set
                {
                    Byte addressSize = 1;

                    if (_pipeId <= (Byte) Pipe.Pipe1)
                    {
                        addressSize = (Byte) (_radio.AddressWidth + 2);
                    }

                    if (value.Length != addressSize)
                    {
                        throw new InvalidAddressSizeException("Invalid address size", addressSize);
                    }

                    _radio.ExecuteCommand(Commands.W_REGISTER, (Registers)(_pipeId + (Byte)Registers.RX_ADDR_P0), addressSize > 1 ? ReverseArray(value) : value);
                }
            }

            /// <summary>
            /// Queue an acknowledge packet for the data pipe
            /// </summary>
            /// <param name="ackPacket">The acknowledge packet</param>
            public void QueueAcknowledgePacket(Byte[] ackPacket)
            {
                _radio.ExecuteCommand((Commands) ((Byte) Commands.W_ACK_PAYLOAD | _pipeId), 0, ackPacket);
            }
        }

        #endregion
    }
}